/*
 * 
 * Copyright (C) 2010 Wilfried Elmenreich, Anita Sobe 
 * This file is part of VideoNetwork.
 * 
 * VideoNetwork is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 
 * You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>. 
 */
package proxy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

import proxy.Configuration.CleanUpType;
import simulation.Simulator;
import simulation.SimulatorConfig.ScenarioType;
import consumer.Consumer;
import consumer.Viewing;

public abstract class ServerNode {

	private int id;

	protected Simulator simulator;

	protected Configuration config;

	protected HormoneControls hCtrl;

	private boolean connected;
	
	private long diskCapacity;

	public void setDiskCapacity(long diskCapacity) {
		this.diskCapacity = diskCapacity;
	}

	/**
	 * auxiliary variable for the hop count mechanism (used by the hopdist
	 * method of the simulator)
	 */
	private int hopLevel;

	/**
	 * Units stored and available at this server
	 */
	private ArrayList<UnitContainer> storedUnits = new ArrayList<UnitContainer>();
	/**
	 * for fast search
	 */
	private SortedMap<Integer, UnitContainer> storedUnitIds = new TreeMap<Integer, UnitContainer>();

	private SortedMap<Content, ArrayList<UnitContainer>> storedContent = new TreeMap<Content, ArrayList<UnitContainer>>();
	private long diskUsedInBytes = 0;

	/**
	 * Units which are in the progress of a transfer operation
	 */
	protected ArrayList<UnitContainer> incomingUnits = new ArrayList<UnitContainer>();

	/**
	 * outgoing units on all links
	 */
	private ArrayList<ArrayList<Migration>> outgoingUnits = new ArrayList<ArrayList<Migration>>();

	/**
	 * List of consumers directly connected to this server
	 */
	private ArrayList<Consumer> consumers = new ArrayList<Consumer>();

	/**
	 * 
	 */
	protected ArrayList<Link> links = new ArrayList<Link>();

	/**
	 * 
	 * @param simulator
	 */
	protected ServerNode(int id, Simulator simulator) {
		this(id, simulator, null, null);
	}

	/**
	 * 
	 * @param simulator
	 * @param config
	 */
	protected ServerNode(int id, Simulator simulator, Configuration config) {
		this(id, simulator, config, null);
	}

	/**
	 * 
	 * @param simulator
	 * @param config
	 * @param hCtrl
	 */
	protected ServerNode(int id, Simulator simulator, Configuration config,
			HormoneControls hCtrl) {
		this.id = id;
		this.simulator = simulator;
		this.config = config;
		// ensure at least default parameters!
		if (config == null)
			this.config = new Configuration();

		this.hCtrl = hCtrl;
		if (hCtrl == null)
			this.hCtrl = new HormoneControls();
		
		this.diskCapacity=config.getDiskCapacity();
	}

	/**
	 * Check if a given node is connected to current server
	 * 
	 * @param n
	 * @return
	 */
	public boolean containsTarget(ServerNode n) {
		for (Link li : links)
			if (li.getTargetNode() == n)
				return true;
		return false;
	}

	/**
	 * 
	 * @param id
	 * @return
	 */
	public UnitContainer searchInIncomingUnits(int id) {
		for (UnitContainer uc : incomingUnits)
			if (uc.getVideoUnit().getId() == id)
				return uc;
		return null;
	}

	public List<UnitContainer> searchInIncomingUnits(Content c) {
		ArrayList<UnitContainer> results = new ArrayList<UnitContainer>();
		for (UnitContainer uc : incomingUnits)
			if (uc.getVideoUnit().getContent().equals(c)) {
				results.add(uc);
			}
		return results;
	}

	public boolean isOneInIncomingUnits(Content c) {
		for (UnitContainer uc : incomingUnits) {
			if (uc.getVideoUnit().getContent().equals(c)) {
				return true;
			}
		}
		return false;
	}

	public boolean isOnePresentUnit(Content c) {
		for (UnitContainer uc : storedUnits) {
			if (uc.getVideoUnit().getContent().equals(c)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * 
	 * @param target
	 * @return
	 */
	public Link getLinkTo(ServerNode target) {
		for (Link li : links)
			if (li.getTargetNode() == target)
				return li;
		return null;
	}

	/**
	 * 
	 * @return
	 */
	public int diskReservedinBytes() {
		int used = 0;
		for (UnitContainer v : incomingUnits)
			used += v.getVideoUnit().getSizeInBytes();
		return used;
	}

	/**
	 * 
	 * @return
	 */
	public int diskFreeinBytes() {
		return (int) (this.getDiskCapacity() - diskUsedInBytes - diskReservedinBytes());
	}

	/**
	 * Gives a double value between 0 and 1 stating the quotient of used and
	 * reserved to available memory on this server
	 * 
	 * @return
	 */
	public double diskUsage() {
		double i = (double) (diskUsedInBytes + diskReservedinBytes())
				/ this.getDiskCapacity();
		return i;
	}

	/**
	 * 
	 * @param n
	 * @param datarate
	 * @param symmetric
	 */
	public void connectTo(ServerNode n, double datarate, boolean symmetric) {
		if (!this.containsTarget(n)) {
			Link li = new Link(n, datarate);
			links.add(li);
			outgoingUnits.add(new ArrayList<Migration>());
		}
		if (symmetric)
			if (!n.containsTarget(this)) {
				Link li = new Link(this, datarate);
				n.links.add(li);
				n.outgoingUnits.add(new ArrayList<Migration>());
			}
	}

	void logUnitList() {

		String log = "S" + this.id + " has ";
		String delim = "";
		for (UnitContainer vc : this.storedUnits) {
			log += delim + vc.getVideoUnit().getId() + " "
					+ vc.getVideoUnit().getContent();
			delim = ",";
		}
		simulator.log(log);
	}

	/**
	 * containedUnit checks if a unit is present on this server
	 * 
	 * @param id
	 *            of the unit
	 * @return if found, the respective UnitContainer object is returned,
	 *         otherwise a null pointer
	 */
	public UnitContainer searchPresentUnit(int id) {
		return storedUnitIds.get(id);
	}

	/**
	 * Checks if a unit with a given tag is present at the server
	 * 
	 * @param c
	 *            the specific content to search for
	 * @return the related units
	 */
	public ArrayList<UnitContainer> searchPresentUnits(Content c) {
		
		ArrayList<UnitContainer> res = storedContent.get(c);
		return res == null ? new ArrayList<UnitContainer>() : res;
	}

	/**
	 * called form migrateVideoUnits every timestep when transfer is finished
	 * put unit to stored units
	 */
	public void migrateVideoUnits() {

		// cleanup units after movement
		for (Iterator<UnitContainer> iter = storedUnits.listIterator(); iter
				.hasNext();) {
			UnitContainer uc = iter.next();
			int id = uc.getVideoUnit().getId();
			if ((uc.isDeleteAfterCopy())
					&& (uc.lastUse() < simulator.getSimTime())) {
				iter.remove();
				storedUnitIds.remove(id);
			}
		}

		// add units to storage if finally transfered from incoming units
		for (Iterator<UnitContainer> iter = incomingUnits.listIterator(); iter
				.hasNext();) {
			UnitContainer uc = iter.next();
			if (simulator.getSimTime() >= uc.copyProcessUntil) {
				// if its not already there
				if (searchPresentUnit(uc.getVideoUnit().getId()) == null) {
					simulator.log("u" + uc.getVideoUnit().getId() + " "
							+ uc.getVideoUnit().getContent()
							+ " has arrived at S" + id);

					addUnitToStorage(uc);
					// reset usage to current time
					uc.setTimeReceived(simulator.getSimTime());
					uc.viewedUntil = simulator.getSimTime();
					uc.copyProcessUntil = simulator.getSimTime();
					// logUnitList();
					iter.remove();
				}
			}
		}
		//
		putMigrationsToOutgoing();
	}

	protected abstract void putMigrationsToOutgoing();

	/**
	 * holds statistics about how many requests were set up for which
	 * keyword/unit
	 * 
	 * @param viewings
	 */
	public abstract void countRequest(ArrayList<Viewing> viewings);

	public abstract void decreaseInterest(Viewing vi);

	/**
	 * step 3: called from sortOutgoing go through migrations and send them to
	 * the incoming queue of the target node
	 * 
	 * @param vu
	 * @param link
	 * @param copy
	 * @return
	 */
	public boolean migrateUnitTo(VideoUnit vu, Link link, boolean copy) {

		if (links.indexOf(link) == -1)
			return false;
		// clean up outgoing units
		ArrayList<Migration> ms = outgoingUnits.get(links.indexOf(link));
		for (int i = 0; i < ms.size(); i++) {
			if (ms.get(i).getVideoUnit().getId() == vu.getId()) {
				ms.remove(i);
			}
		}
		UnitContainer uc;
		uc = searchPresentUnit(vu.getId());

		if (uc == null)
			return false;

		if (link == null)
			return false;

		// failure if target server is not directly linked
		ServerNode target = link.getTargetNode();

		simulator
				.log("S" + this.id + ": trying to copy/move unit u"
						+ vu.getId() + " " + vu.getContent() + " to S"
						+ target.getId());

		// check if unit already present at target server
		boolean alreadypresent = false;
		if (simulator.getScenario() == ScenarioType.SEARCH
				|| simulator.getScenario() == ScenarioType.UNITSIZE) {
			if (target.searchPresentUnits(vu.getContent()).size() > 0) {
				alreadypresent = true;
			}
		} else {
			if (target.searchPresentUnit(vu.getId()) != null) {
				alreadypresent = true;
			}
		}
		if (alreadypresent) {// perhaps we can get rid of the unit
			simulator.log("u" + vu.getId() + " " + vu.getContent()
					+ " already present at S" + target.id);
			return false;
		}
		alreadypresent = false;
		// check if this unit is already transfered to target server
		if (simulator.getScenario() == ScenarioType.SEARCH
				|| simulator.getScenario() == ScenarioType.UNITSIZE) {
			if (target.searchInIncomingUnits(vu.getContent()).size() > 0) {
				alreadypresent = true;
			}
		} else {
			if (target.searchInIncomingUnits(vu.getId()) != null) {
				alreadypresent = true;
			}
		}
		if (alreadypresent) {
			simulator.log("u" + vu.getId() + " " + vu.getContent()
					+ " already transfered to target S" + target.id);
			return false;
		}
		// check capacity at target server
		double freeCap = target.getDiskCapacity() - target.getDiskUsedInBytes()
				- target.diskReservedinBytes();
		boolean result = true;

		while (freeCap < vu.getSizeInBytes() && result == true) {
			simulator.log("S" + this.id
					+ ":not enough space, try to get rid of units. FreeCap: "
					+ freeCap + " u: " + vu.getSizeInBytes() + ", "
					+ vu.getContent());
			result = replace();
			freeCap = target.getDiskCapacity() - target.getDiskUsedInBytes()
					- target.diskReservedinBytes();
		}
		if (freeCap < vu.getSizeInBytes()) {
			simulator.log("S" + this.id + ": not enough space at target S"
					+ target.id + " freeCap: " + freeCap + " u:"
					+ vu.getSizeInBytes() + " " + vu.getContent());
			return false;
		}

		if (copy)
			simulator.migrationCopyCounter++;
		else
			simulator.migrationMoveCounter++;

		if (simulator.getSimTime() > link.getOccupiedUntil())
			link.setOccupiedUntil(simulator.getSimTime());

		double occupiedTime = vu.getSizeInBytes() / link.getDatarate();
		link.addOccupiedUntil(occupiedTime);
		// occupation in per cent
		// simulator.occupation.add(occupiedTime);
		// set deleteAfterCopy if everything is fine
		uc.setDeleteAfterCopy(!copy);
		// create new unitContainer for the target node
		UnitContainer tuc = new UnitContainer(vu, link.getOccupiedUntil());
		tuc.numHopsTraveled = uc.numHopsTraveled + 1;
		target.incomingUnits.add(tuc);

		if (copy)
			simulator.log("S" + id + ": copy u" + vu.getId() + " "
					+ vu.getContent() + " to S" + target.id + " until "
					+ link.getOccupiedUntil());
		else
			simulator.log("S" + id + ": move u" + vu.getId() + " "
					+ vu.getContent() + " to S" + target.id + " until "
					+ link.getOccupiedUntil());
		return true;
	}

	private long getDiskCapacity() {
		return this.diskCapacity;
	}
	
	public Configuration getConfig(){
		return config;
	}

	public boolean replace() {
		if (config.getCleanup() == CleanUpType.LRU) {
			LRUcleanUp();
		} else if (config.getCleanup() == CleanUpType.LFU) {
			return performLFUCleanup();
		} else if (config.getCleanup() == CleanUpType.HORMONE) {
			return performHormoneCleanup();
		} else if (config.getCleanup() == CleanUpType.THREEWAY) {
			for (int i = 0; i < storedUnits.size(); i++) {
				UnitContainer uc = storedUnits.get(i);
				if (simulator.getSimTime() - uc.lastUse() > hCtrl
						.getCleanUpTriggerTime()) {
					return performThreeWayCleanupForUnit(uc.getVideoUnit()
							.getId());
				}
			}
		}
		return false;
	}

	/**
	 * remove unused videos on the server in order to save disk space
	 */
	public void cleanUp() {
		if (config.isUseLRUcleanUp()) {
			LRUcleanUp();
			return;
		}

		if (diskUsage() < config.getDiskfullCleanupLimit())
			return;
		if (config.getCleanup() == CleanUpType.THREEWAY) {
			for (int i = 0; i < storedUnits.size(); i++) {
				UnitContainer uc = storedUnits.get(i);
				if (simulator.getSimTime() - uc.lastUse() > hCtrl
						.getCleanUpTriggerTime()) {
					if (performThreeWayCleanupForUnit(uc.getVideoUnit().getId())) {
						// only need to delete one
						break;
					}
				}
			}
		} else if (config.getCleanup() == CleanUpType.LFU) {
			performLFUCleanup();
		} else if (config.getCleanup() == CleanUpType.HORMONE) {
			performHormoneCleanup();
		}

	}

	boolean performLFUCleanup() {
		return false;
		// TODO
	}

	boolean performHormoneCleanup() {
		return false;
		// TODO
	}

	/**
	 * 
	 * @param id
	 * @return returns if unit could be deleted, otherwise unit will be moved
	 */
	abstract boolean performThreeWayCleanupForUnit(int id);

	/**
	 * 
	 * @param id
	 * @return
	 */
	protected int countUnitOnNeighbors(int id) {
		int count = 0;
		for (Link li : links) {
			UnitContainer uc = li.getTargetNode().searchPresentUnit(id);
			if (uc != null) {
				if (!uc.isDeleteAfterCopy())
					count++;
			} else if (li.getTargetNode().searchInIncomingUnits(id) != null)
				count++;
		}
		return count;
	}

	protected int countUnitOnNeighbors(Content content) {
		int count = 0;
		for (Link li : links) {
			int size = li.getTargetNode().searchPresentUnits(content).size();
			if (size > 0) {
				count++;
			} else if (li.getTargetNode().searchInIncomingUnits(content).size() > 0)
				count++;
		}
		return count;
	}

	public abstract void increaseInterest(List<Viewing> currentlyDemanding,
			boolean newRequest, boolean sequential);

	@SuppressWarnings("unchecked")
	private boolean LRUcleanUp() {
		if (diskUsage() < config.getDiskfullCleanupLimit())
			return false;
		ArrayList<UnitContainer> localCopy = (ArrayList<UnitContainer>) storedUnits
				.clone();
		Collections.sort(localCopy); // sort according to last use from least
		// recently used to most recently used

		while (diskUsage() >= config.getDiskfullCleanupLimit()) {
			boolean deleteWorked = false;
			for (int i = 0; i < localCopy.size(); i++) {
				UnitContainer current = localCopy.get(i);
				int id = current.getVideoUnit().getId();
				if (countUnitOnNeighbors(id) > 1) {
					// remove this unit
					localCopy.remove(current);
					removeUnitFromStorage(current);
					simulator.countRemoved++;
					simulator.log("S" + id + ": removed u" + id + " "
							+ current.getVideoUnit().getContent());
					logUnitList();
					deleteWorked = true;
					break;
					// was immediately deleted
				}
			}
			if (deleteWorked == false)
				simulator.countCleanupFailed++;
			return deleteWorked; // we cannot delete any more units, they are
									// all pinned
		}
		return false;
	}

	/**
	 * step2: called from putMigrationsToOutgoing Sorts outgoing units according
	 * to the hormone levels calls step 3: migrateUnitTo
	 */
	protected void sortOutgoing() {
		// sort according to importance
		for (int i = 0; i < getOutgoingUnits().size(); i++) {
			Collections.sort(getOutgoingUnits().get(i),
					Collections.reverseOrder());
		}

		// try to send on all links
		for (int i = 0; i < getOutgoingUnits().size(); i++) {
			for (int j = 0; j < getOutgoingUnits().get(i).size(); j++) {
				Migration m = getOutgoingUnits().get(i).get(j);
				migrateUnitTo(m.getVideoUnit(), m.getLink(), m.isCopy());
			}
		}
	}

	public void removeUnitFromStorage(UnitContainer uc) {
		int id = uc.getVideoUnit().getId();
		storedUnits.remove(uc);
		storedUnitIds.remove(id);
		removeContentFromStorage(uc.getVideoUnit().getContent(), uc);
		diskUsedInBytes -= uc.getVideoUnit().getSizeInBytes();
	}

	public void removeContentFromStorage(Content c, UnitContainer uc){
		storedContent.get(c).remove(uc);
	}
	
	public void cleanupMigrations() {

		for (Iterator<UnitContainer> iter = storedUnits.listIterator(); iter
				.hasNext();) {
			UnitContainer uc = iter.next();
			if ((uc.isDeleteAfterCopy())
					&& (uc.lastUse() < simulator.getSimTime())) {
				iter.remove();
			}
		}
	}

	public void addUnitToStorage(UnitContainer uc) {
		storedUnits.add(uc);
		storedUnitIds.put(uc.getVideoUnit().getId(), uc);
		Content c = uc.getVideoUnit().getContent();
		addContentToStorage(c, uc);
		diskUsedInBytes += uc.getVideoUnit().getSizeInBytes();
	}
	
	public void addContentToStorage(Content c, UnitContainer uc){
		if (!storedContent.containsKey(c)) {
			storedContent.put(c, new ArrayList<UnitContainer>());
		}
		storedContent.get(c).add(uc);
	}

	public Iterator<UnitContainer> getStoredUnitIterator() {
		return storedUnits.iterator();
	}

	// GETTERS AND SETTERS START HERE

	// public ArrayList<UnitContainer> getStoredUnits(){
	// return storedUnits;
	// }

	public ArrayList<Link> getLinks() {
		return links;
	}

	public int getId() {
		return id;
	}

	public ArrayList<Consumer> getConsumers() {
		return consumers;
	}

	public ArrayList<ArrayList<Migration>> getOutgoingUnits() {
		return outgoingUnits;
	}

	public int getHopLevel() {
		return hopLevel;
	}

	public void setHopLevel(int hopLevel) {
		this.hopLevel = hopLevel;
	}

	public boolean isConnected() {
		return connected;
	}

	public void setConnected(boolean connected) {
		this.connected = connected;
	}

	public long getDiskUsedInBytes() {
		return diskUsedInBytes;
	}

	public void removeNeighbor(ServerNode node) {
		Link li = null;
		if ((li = getLinkTo(node)) != null) {
			int index = links.indexOf(li);
			links.remove(index);
			outgoingUnits.remove(index);

		}
	}
}

class BestConnection {
	double highestPhero;
	Link bestLink;
	BestConnection bestGeneralConnection;
}